Skip to content

Fix requirements.txt pins not matched under PEP 440 (#475) - #478

Merged
Mikola Lysenko (mikolalysenko) merged 5 commits into
mainfrom
agent/fix-requirements-pep440-pin-match
Oct 2, 2026
Merged

Mikola Lysenko (mikolalysenko) merged 5 commits into
mainfrom
agent/fix-requirements-pep440-pin-match

Conversation

@mikolalysenko

@mikolalysenko Mikola Lysenko (mikolalysenko) commented Oct 1, 2026 •

Copy link
Copy Markdown
Collaborator

LLM Description written by Claude Code:claude-opus-5-5

Fixes #475

Summary

Hand-written requirements.txt pins like six==1.16 (for an installed six 1.16.0) are now matched the way pip matches them, under PEP 440 equality. Before this change, scan --mode hosted skipped such a pin, reported "no requirements.txt entry", exited 0, and left the project installing the unpatched release. This is a regression from v4.0.0, introduced in #239.

Root cause

Every pin matcher compared the pinned version with the patch version as a raw string. pip compares them under PEP 440, which zero-pads release segments and ignores leading zeros, case and alternate pre/post/dev spellings. So ==1.16, ==1.16.0.0 and ==01.16.0 all select exactly 1.16.0. The same raw comparison was in three writers:

  • hosted patch/redirect/requirements.rs skipped the pin with redirect_requirements_entry_not_found and exited 0;
  • vendored vendor/pypi_requirements.rs refused with a false pypi_requirement_not_pinned;
  • Hatch utils/hatch.rs (used by hosted and vendored Hatch) refused hand-written pyproject.toml pins like urllib3==1.26.18.0 with "requires an exact ==1.26.18 declaration". Same cause at the same boundary, so it is fixed here too.

Fix

  • New utils/pep440.rs: a small parser for the packaging version grammar, plus PEP 440 equality (epoch, zero-trimmed release, normalized pre/post/dev, case-folded local segments). Digit runs are compared as leading-zero-trimmed strings, so long numbers can't overflow. Invalid versions are never equal to anything, so callers fail closed.
  • All three writers now use it for == pins. === (arbitrary equality) keeps plain string comparison, as PEP 440 defines it, and wildcards (==1.*) are still ranges.

Notes / follow-ups

  • Rollback spelling: hosted mode keeps no ledger in v5, so hosted rollback re-derives name==<patch version>. A redirected six==1.16 therefore rolls back to six==1.16.0, which installs the same release. This matches how rollback already normalizes six == 1.16.0 today. Vendored revert is ledger-based and stays byte-exact.
  • Lock-only discovery (no venv): utils/requirements.rs::exact_pin still carries the spelled version into the purl (pkg:pypi/six@1.16). Matching that to the 1.16.0 release needs PyPI's canonical release spelling, not just local normalization. The issue lists this as a "may" and its repro uses an installed venv, so it's left as a follow-up rather than guessed at here.

Test evidence

Regression tests, each shown failing on main and passing with the fix:

Issue variant Test main this PR
hosted ==X.Y, ==X.Y.Z.0, ==0X.Y.Z, spaced + marker patch::redirect::requirements::tests::pep440_equivalent_pins_are_rewritten ❌ redirect_requirements_entry_not_found ✅
hosted, end to end (get <uuid> --mode hosted over requests==2.31, ==2.31.0.0, Requests==02.31.0) in_process_get_hosted_ecosystems::pypi_requirements_hosted_rewrites_pep440_equivalent_pin ❌ file unchanged ✅
vendored false pypi_requirement_not_pinned vendor::pypi_requirements::tests::find_pin_classifies_every_shape (new PEP 440 cases; === / wildcard still Range) ❌ ✅
Hatch equivalent pins utils::hatch::tests::pep440_equivalent_pins_are_exact_declarations ❌ ✅
helper utils::pep440::tests::* (equal spellings, non-equal releases, invalid input, == vs ===/wildcards) new ✅

Local runs (Linux):

  • cargo clippy --workspace --all-features -- -D warnings: clean.
  • cargo test --workspace --all-features --no-fail-fast: everything passes except 12 permission-based write-failure tests, which fail because this sandbox runs as uid 0 (root ignores read-only bits). They fail identically on main, and all 12 pass when re-run as uid 65534.
  • cargo test -p socket-patch-cli --all-features --test e2e_vendor_pypi_build -- --ignored (real uv + pip + PyPI): 7/7 pass.
  • cargo fmt: the changed code is rustfmt-formatted. CI has no fmt step and main itself isn't fmt-clean (rustfmt 1.8 rewrites ~125 unrelated files), so unrelated reflows were left out.
  • No wrapper changes: npm/, pypi/ and gem/ only dispatch to the binary.

🤖 Generated with Claude Code


Note

Medium Risk
Changes pin-matching on the patch redirect path for PyPI; incorrect PEP 440 logic could mis-redirect or skip pins, but invalid versions fail closed and ===/wildcards are unchanged.

Overview
Fixes #475: PyPI pin matching now treats == the same way pip/uv/Hatch do under PEP 440, instead of comparing version strings literally.

Adds utils/pep440 with packaging-style parsing and equality (versions_equal, is_exact_pin_of). Hosted requirements.txt redirect, vendored pypi_requirements pin discovery, and Hatch pyproject.toml rewrites all use it for == pins. === stays plain string equality; wildcards and ranges are unchanged.

Pins like requests==2.31, ==2.31.0.0, or ==02.31.0 now redirect to the patched wheel when the grant is 2.31.0, instead of skipping with “no entry” / false “not pinned” and leaving the project unpatched.

Regression coverage: unit tests for the helper and each rewriter, plus an in-process get --mode hosted test over equivalent requirements.txt pins.

Reviewed by Cursor Bugbot for commit bcc5335. Configure here.


Generated by Claude Code

Assisted-by: Claude Code:claude-opus-5-5
A hand-written pin such as `six==1.16` installs six 1.16.0, but the
hosted requirements.txt rewrite compared versions as raw strings and
skipped it, so `scan` exited 0 and the project stayed unpatched. The
vendored requirements writer and the Hatch rewriter refused the same
pins as "not pinned".

Add a small PEP 440 equality helper (zero-padded release segments,
leading zeros, case and pre/post/dev spellings) and use it for `==`
pins in all three writers. `===` keeps plain string equality, as PEP
440 defines it.

Fixes #475

Assisted-by: Claude Code:claude-opus-5-5
Mirrors the #475 repro end to end: `get <uuid> --mode hosted` over
`requests==2.31`, `==2.31.0.0` and `Requests==02.31.0` must redirect
the pin to the hosted wheel. Fails on main, passes with the fix.

Assisted-by: Claude Code:claude-opus-5-5
@mikolalysenko
Mikola Lysenko (mikolalysenko) marked this pull request as ready for review October 1, 2026 16:03
@mikolalysenko

Copy link
Copy Markdown
Collaborator Author

BugBot review


Generated by Claude Code

@cursor cursor Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale Bugbot comment from a previous run.

@mikolalysenko Mikola Lysenko (mikolalysenko) added the Ready for review Agent-verified: mergeable, CI green, Bugbot clean — awaiting human review label Oct 1, 2026
@mikolalysenko

Copy link
Copy Markdown
Collaborator Author

Burn-down agent: ready for review at bcc5335 (bcc533518a74ca2234958cb56379a4333e97d6cb).

  • CI: 97/97 non-skipped checks green (3 skipped).
  • Bugbot: reviewed bcc5335, no findings; no unresolved review threads.
  • Mergeable with no conflicts (15 commits behind main, merges cleanly).
  • Reviewer focus: the new utils/pep440.rs equality rules, and the note that hosted rollback re-derives name==<patch version> (so six==1.16 rolls back as six==1.16.0).

Generated by Claude Code

@mikolalysenko

Copy link
Copy Markdown
Collaborator Author

Reviewed bcc533518a74ca2234958cb56379a4333e97d6cb. Recommendation: ready to merge from code review. No actionable correctness or security regressions found. Checked the parser against PyPA's version grammar, the normalization and invalid-input boundaries, == versus ===, and all three writer integrations.

Validation: 5 PEP 440-focused core tests, all 36 vendored-requirements unit tests, and the in-process hosted-get regression passed. Full workspace and real pip/uv matrices were not rerun. The separately documented lock-only discovery follow-up remains outside this change.

@Tanmay182003

Copy link
Copy Markdown

Since #383, a hosted scan of a requirements.txt that is not in
hash-checking mode pins the patched wheel with the url's #sha256=
fragment rather than --hash, so the PEP 440 regression test now
expects that form, matching the existing hosted pypi test.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CVbFzTeYSTvB5iKjY6FRg7
@mikolalysenko

Copy link
Copy Markdown
Collaborator Author

Tanmay Singla (@Tanmay182003) Good catch, thanks. Fixed in c331d18. I merged main into the branch (9633810, a merge commit rather than a rebase) and changed the expectation at in_process_get_hosted_ecosystems.rs:311 to requests @ {url}#sha256={SHA256}, the same form #383 uses in the existing hosted pypi test.

Checked locally on the merged tree:

  • in_process_get_hosted_ecosystems: 8/8 pass.
  • socket-patch-core lib: all pass except 4 permission-based tests that only fail because this sandbox runs as root. They pass when run unprivileged.
  • cargo clippy --workspace --all-features -- -D warnings is clean.

The existing --hash= assertions in redirect/requirements.rs are for inputs that are already in hash-checking mode, so #383 still writes --hash for those and they pass unchanged.


Generated by Claude Code

@mikolalysenko

Copy link
Copy Markdown
Collaborator Author

BugBot review


Generated by Claude Code

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Bugbot reviewed your changes and found no new issues!

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit c331d18. Configure here.

@mikolalysenko

Copy link
Copy Markdown
Collaborator Author

CI: native (macos-latest, 1.1.45) failed on c331d18. One case of the bun native backtest failed: space-unicode hosted FAIL ['refusalCodesExact']. The other 43 cases passed (job).

This doesn't look like it comes from this PR:

  • The PR only changes pypi matching (utils/pep440.rs, redirect/requirements.rs, vendor/pypi_requirements.rs, utils/hatch.rs), and the bun hosted path never calls any of it.
  • The same cell passed on main at d63ae5f, which is exactly what the branch merged. It also passed on the previous head, bcc5335.
  • On this commit, bun 1.1.45 on ubuntu passed, and every other macOS bun version that has finished passed.

I couldn't see which refusal code appeared, because the log only records the check name and the artifact isn't reachable from my sandbox. No fix for it exists yet. I'll re-run the failed job once the workflow finishes, since GitHub won't re-run it while the rest of the run is still going. If it fails again, I'll treat it as a real failure and dig in.


Generated by Claude Code

@mikolalysenko

Copy link
Copy Markdown
Collaborator Author

[agent] All checks green on c331d18: 297 passed, 3 skipped. native (macos-latest, 1.1.45) passed when I re-ran it once, so the earlier space-unicode hosted refusal-code failure didn't repeat. It's the Bun backtest flake that #565 is fixing. Bugbot reviewed c331d18 and found nothing, and there are no unresolved threads. Locally, clippy is clean and the workspace tests pass, apart from 3 covgap_commands_vendor read-only-dir tests that only fail because my sandbox runs as root. That file isn't touched by this PR. Tanmay Singla (@Tanmay182003), could you re-check now that the #sha256= expectation is fixed?


Generated by Claude Code

@mikolalysenko
Mikola Lysenko (mikolalysenko) merged commit bf0e0d1 into main Oct 2, 2026
538 of 539 checks passed
@mikolalysenko
Mikola Lysenko (mikolalysenko) deleted the agent/fix-requirements-pep440-pin-match branch October 2, 2026 16:23
@mikolalysenko

Copy link
Copy Markdown
Collaborator Author

Second pass reviewed c331d185c09f449cd6238786fb2cef9b869802ad: ready from code review; no additional code change needed. I checked the clean main merge and the #sha256= expectation update against the hosted requirements writer. The PEP 440 matching paths remain consistent with the merged hash-pinning behavior.

Validation at this exact head: in_process_get_hosted_ecosystems::pypi_requirements_hosted_rewrites_pep440_equivalent_pin passed, including all equivalent-pin variants. The earlier 5 PEP 440 and 36 vendored-requirements tests were not repeated in this incremental pass. No new blocking finding.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Ready for review Agent-verified: mergeable, CI green, Bugbot clean — awaiting human review

Projects

None yet

3 participants